using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Net;
using System.Xml;
using System.Xml.Serialization;

namespace TracClient
{
  public partial class Form1 : Form
  {
    Dictionary<string, Ticket> tickets = new Dictionary<string, Ticket>();
    Dictionary<string, string> customTitles = new Dictionary<string, string>();
    XmlNamespaceManager ns = null;
    string filter = string.Empty;

    public Form1()
    {
      InitializeComponent();
      topView.Panel2MinSize = 250;
      topView.Panel2Collapsed = true;
      dataGridView1.SortCompare += new DataGridViewSortCompareEventHandler(dataGridView1_SortCompare);
      searchBox.Enabled = false;
      searchBox.Text = "Loading tickets...";
    }

    void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
    {
      if (e.Column.HeaderText == "Created Date" || e.Column.HeaderText == "Modified Date")
      {
        string id1 = dataGridView1["ticket", e.RowIndex1].Value.ToString();
        string id2 = dataGridView1["ticket", e.RowIndex2].Value.ToString();
        DateTime d1 = DateTime.Parse(tickets[id1].GetValueRaw(e.Column.Name));
        DateTime d2 = DateTime.Parse(tickets[id2].GetValueRaw(e.Column.Name));
        e.SortResult = (d1 == d2 ? 0 : (d1 > d2 ? -1 : 1));
        e.Handled = true;
        return;
      }
    }

    string cookie = string.Empty;
    string auth = string.Empty;
    string LoadPage(string url)
    {
      WebRequest req = WebRequest.Create(url);
      WebResponse resp = null;
      req.Credentials = new NetworkCredential(Config.main.user, Config.main.pass);
      string offline = "[OFFLINE] ";
      try
      {
        resp = req.GetResponse();
        if (Text.StartsWith(offline))
          Text = Text.Remove(0, offline.Length);
      }
      catch(WebException ex)
      {
        if (ex.Status == System.Net.WebExceptionStatus.Timeout && !Text.StartsWith(offline))
          Text = offline + Text;
      }
      cookie = resp.Headers["Set-Cookie"];
      auth = req.Headers["Authorization"];
      string s = string.Empty;
      using (StreamReader r = new StreamReader(resp.GetResponseStream()))
        s = r.ReadToEnd();
      return s;
    }

    XmlNode LoadTicket(string id)
    {
      string s = tickets[id].Details;
      if (tickets[id].Unread || s.Length == 0)
      {
        s = LoadPage(string.Format(Config.main.site + "ticket/{0}?format=rss", id));
        tickets[id].Details = s;
        SaveTickets();
      }
      XmlDocument xml = new XmlDocument();
      xml.LoadXml(s);
      ns = new XmlNamespaceManager(xml.NameTable);
      ns.AddNamespace("dc", "http://purl.org/dc/elements/1.1/");
      return xml.SelectSingleNode("/");
    }

    void SaveTickets()
    {
      string file = "tickets.xml";
      List<Ticket> tickets_ = new List<Ticket>();
      foreach (KeyValuePair<string, Ticket> ticket_ in tickets)
        tickets_.Add(ticket_.Value);
      XmlSerializer serializer = new XmlSerializer(tickets_.GetType());
      using (StreamWriter writer = new StreamWriter(file))
        serializer.Serialize(writer, tickets_);
    }

    List<string> LoadTicketListString()
    {
      List<string> ret = new List<string>();
      string page = LoadPage(Config.main.site + Config.main.listUrl + (Config.main.appendUserToListUrl ? Config.main.user : ""));
      using (StringReader r = new StringReader(page))
      {
        string s = r.ReadLine();
        ret.Add(s);
        while (r.Peek() != -1)
        {
          s = r.ReadLine();
          if (s.Length > 7 && s.Substring(1, 7).Equals(",Owned,", StringComparison.OrdinalIgnoreCase))
            ret.Add(s);
          else
            ret[ret.Count - 1] += "<br>" + s;
        }
      }
      return ret;
    }

    string CustomTitle(string id)
    {
      return customTitles.ContainsKey(id) ? customTitles[id] : string.Empty;
    }

    void UpdateRowFromTicket(string id, int row)
    {
      fillingGrid = true;
      if (!tickets[id].Unread)
        dataGridView1.Rows[row].DefaultCellStyle = dataGridView1.DefaultCellStyle;
      else
      {
        DataGridViewCellStyle style = new DataGridViewCellStyle();
        style.Font = new Font(dataGridView1.Font, FontStyle.Bold);
        style.BackColor = Color.FromArgb(238, 238, 238);
        dataGridView1.Rows[row].DefaultCellStyle = style;
      }

      foreach (DataGridViewColumn col in dataGridView1.Columns)
        if (col.Name == "summary" && CustomTitle(id).Length > 0)
        {
          dataGridView1[col.Index, row].Value = CustomTitle(id);
          DataGridViewCellStyle style2 = new DataGridViewCellStyle(dataGridView1.Rows[row].DefaultCellStyle);
          style2.ForeColor = Color.DarkRed;
          dataGridView1.Rows[row].DefaultCellStyle = style2;
        }
        else
        {
          dataGridView1[col.Index, row].Value = tickets[id].GetValue(col.Name);
          dataGridView1.Rows[row].DefaultCellStyle = dataGridView1.Rows[row].DefaultCellStyle;
        }
      fillingGrid = false;
    }

    bool fillingGrid = false;
    void FillGrid()
    {
      fillingGrid = true;
      dataGridView1.Columns.Clear();
      dataGridView1.Columns.Add("ticket", "Id");
      dataGridView1.Columns.Add("summary", "Title");
      dataGridView1.Columns.Add("priority", "Priority");
      dataGridView1.Columns.Add("stage", "Status");
      dataGridView1.Columns.Add("created", "Created Date");
      dataGridView1.Columns.Add("_changetime", "Modified Date");
      //dataGridView1.Columns.Add("_description", "Description");
      dataGridView1.Columns.Add("_reporter", "Creaded by");
      dataGridView1.Rows.Clear();
      foreach (KeyValuePair<string, Ticket> ticket_ in tickets)
      {
        Ticket ticket = ticket_.Value;
        string id = ticket.GetValue("ticket");
        bool statusIsClosed = (ticket.GetValueRaw("stage") == "Closed");
        bool statusIsFixed = (ticket.GetValueRaw("stage") == "Fixed");
        bool ticketContainsFilter = (filter.Length == 0 || ticket.Contains(filter));
        ticketContainsFilter = ticketContainsFilter || CustomTitle(id).Length > 0 && CustomTitle(id).IndexOf(filter, StringComparison.OrdinalIgnoreCase) != -1;
        bool statusFilterPassed = (!Config.main.hideTicketsWithClosedStatus || !statusIsClosed);
        statusFilterPassed = statusFilterPassed && (!Config.main.hideTicketsWithFixedStatus || !statusIsFixed);
        if (statusFilterPassed && ticketContainsFilter)
        {
          int row = dataGridView1.Rows.Add();
          UpdateRowFromTicket(ticket.GetValueRaw("ticket"), row);
        }
      }
      //dataGridView1.Rows.RemoveAt(dataGridView1.Rows.Count - 1);
      foreach (DataGridViewColumn col in dataGridView1.Columns)
        col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
      dataGridView1.Columns[dataGridView1.Columns.Count - 1].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
      fillingGrid = false;
    }

    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
      changeOwnerBtn.Enabled = (dataGridView1.SelectedRows.Count > 0);
      addCommentBtn.Enabled = (dataGridView1.SelectedRows.Count > 0);
      changeStatusBtn.Enabled = (dataGridView1.SelectedRows.Count > 0);
      markAsReadBtn.Enabled = (dataGridView1.SelectedRows.Count > 0);
      setTitleBtn.Enabled = (dataGridView1.SelectedRows.Count == 1);
      topView.Panel2Collapsed = true;
      if (fillingGrid || dataGridView1.SelectedRows.Count == 0)
        return;
      if (dataGridView1.SelectedRows.Count > 1)
      {
        string html = "<style  TYPE='text/css'><!-- table {font-family:'Segoe UI',Verdana,Arial;font-size:18px} --></style>";
        html += string.Format("<table width=100% height=100%><tr><td align=center>{0} rows selected.</td></tr></table>", dataGridView1.SelectedRows.Count);
        webBrowser1.DocumentText = html;
        return;
      }
      RefreshTicket();
    }

    private void RefreshTicket()
    {
      if (dataGridView1.SelectedRows.Count == 0)
        return;
      int row = dataGridView1.SelectedRows[0].Cells[0].RowIndex;
      string id = dataGridView1["ticket", row].Value.ToString();
      ShowTicket(id);
    }

    int GetRow(string id)
    {
      for (int i = 0; i < dataGridView1.RowCount; ++i)
        if (dataGridView1["ticket", i].Value.ToString() == id)
          return i;
      return -1;
    }

    void GotoTicket(string id)
    {
      int i = GetRow(id);
      if (i == -1)
        return;
      dataGridView1.ClearSelection();
      dataGridView1["ticket", i].Selected = true;
      dataGridView1.FirstDisplayedScrollingRowIndex = i;
    }

    void ShowTicket(string id)
    {
      XmlNode xmlTicket = LoadTicket(id);
      XmlNodeList entries = xmlTicket.SelectNodes("rss/channel/item");
      string html = string.Empty;
      html += "<style  TYPE='text/css'><!-- table {font-family:'Segoe UI',Verdana,Arial;font-size:18px} --></style>";
      html += "<table width=100% bgcolor='#eeeeee'><tr>";
      html += string.Format("<td width='35%'>{0}</td><td align='center'>{1}</td><td width='25%' align='right'>{2}</td></tr>", tickets[id].GetValue("_reporter"), DateTime.Parse(tickets[id].GetValueRaw("created")).ToString("MMM d (HH:mm)"), tickets[id].GetValue("priority"));
      html += "<tr><td colspan='3'><hr>" + tickets[id].GetValue("_description") + "</td></tr></table><br>";

      for (int i = 0; i < entries.Count; ++i)
      {
        string date = entries[i].SelectSingleNode("pubDate").InnerText;
        string text = entries[i].SelectSingleNode("description").InnerText;
        string user = entries[i].SelectSingleNode("dc:creator", ns).InnerText;
        string state = string.Empty;
        string prio = string.Empty;
        string attachment = string.Empty;
        date = DateTime.Parse(date).ToString("MMM d (HH:mm)");
        text = text.Replace("<p>", "").Replace("</p>", "<br>");
        text = text.Replace("<em>", "").Replace("</em>", "");
        string text2 = string.Empty;
        for (int j = 0; j < text.Length; ++j)
          if (text[j] != ' ' || text2.Length > 0 && text2[text2.Length - 1] != ' ')
            text2 += text[j];

        if (text2.StartsWith("\n <ul>\n "))
          text2 = text2.Remove(0, 8);
        if (text2.StartsWith("<li><strong>owner</strong>\n changed from "))
        {
          int pos1 = text2.IndexOf(" to ", 41);
          string user1 = text2.Substring(41, pos1 - 41);
          int pos2 = text2.IndexOf("\n </li>\n ", pos1 + 4);
          string user2 = text2.Substring(pos1 + 4, pos2 - pos1 - 4);
          text2 = text2.Substring(pos2 + 9);
          user = string.Format("{0} &rarr; {1}", user1, user2);
        }
        if (text2.StartsWith("</ul>\n "))
          text2 = text2.Remove(0, 7);
        if (text2.StartsWith("<li><strong>priority</strong>\n changed from "))
        {
          int pos1 = text2.IndexOf(" to ", 44);
          string prio1 = text2.Substring(44, pos1 - 44);
          int pos2 = text2.IndexOf("\n </li>\n ", pos1 + 4);
          string prio2 = text2.Substring(pos1 + 4, pos2 - pos1 - 4);
          text2 = text2.Substring(pos2 + 9);
          prio = string.Format("{0} &rarr; {1}", prio1, prio2);
        }
        if (text2.StartsWith("</ul>\n "))
          text2 = text2.Remove(0, 7);
        if (text2.StartsWith("<li><strong>custom_stage</strong>\n changed from "))
        {
          int pos1 = text2.IndexOf(" to ", 48);
          string state1 = text2.Substring(48, pos1 - 48);
          int pos2 = text2.IndexOf("\n </li>\n ", pos1 + 4);
          string state2 = text2.Substring(pos1 + 4, pos2 - pos1 - 4);
          text2 = text2.Substring(pos2 + 9);
          state = string.Format("{0} &rarr; {1}", state1, state2);
        }
        if (text2.StartsWith("</ul>\n "))
          text2 = text2.Remove(0, 7);
        if (text2.StartsWith("<li><strong>attachment</strong>\n set to "))
        {
          int pos2 = text2.IndexOf("\n </li>");
          attachment = text2.Substring(40, pos2 - 40);
          text2 = text2.Substring(pos2 + 7);
        }
        if (text2.StartsWith("\n </ul>\n "))
          text2 = text2.Remove(0, 9);
        html += "<table width=100% bgcolor='#eeeeee'><tr>";
        if (prio.Length > 0)
          state += (state.Length > 0 ? "<br>" : "") + prio;
        html += string.Format("<td width='35%'>{0}</td><td align='center'>{1}</td><td width='25%' align='right'>{2}</td></tr>", user, date, state);
        if (text2.Length > 0 || attachment.Length > 0)
        {
          html += "<tr><td colspan='3'><hr>";
          if (attachment.Length > 0)
            html += "<a href='" + Config.main.site + "raw-attachment/ticket/" + id + "/" + attachment + "'>" + attachment + "</a><br>";
          html += text2;
          html += "</td></tr>";
        }
        html += "</table>";
        html += "<br>";
      }
      webBrowser1.DocumentText = html;
    }

    string MyEscape(string url)
    {
      string ret = string.Empty;
      string hexChars = "0123456789ABCDEF";
      for (int i = 0; i < url.Length; ++i)
        if (url[i] < 'A')
          ret += "%" + hexChars[((int)url[i]) / 16].ToString() + hexChars[((int)url[i]) % 16].ToString();
        else
          ret += url[i];
      return ret;
    }

    void ChangeTicket(string id, string comment, string state, string forwardToUser)
    {
      string postData = string.Empty;
      page = LoadPage(Config.main.site + "ticket/" + id);
      int pos1 = page.IndexOf("__FORM_TOKEN\" value=\"") + 21;
      int pos2 = page.IndexOf('"', pos1 + 1);
      string token = page.Substring(pos1, pos2 - pos1);
      pos1 = page.IndexOf("name=\"ts\" value=\"") + 17;
      pos2 = page.IndexOf('"', pos1 + 1);
      string ts = page.Substring(pos1, pos2 - pos1);

      postData = "__FORM_TOKEN=" + MyEscape(token);
      postData += "&comment=" + MyEscape(comment);
      if (state.Length > 0)
      postData += "&field_custom_stage=" + state;
      postData += "&action=" + (forwardToUser.Length == 0 ? "leave" : "reassign");
      if (forwardToUser.Length > 0)
        postData += "&action_reassign_reassign_owner=" + MyEscape(forwardToUser);
      postData += "&ts=" + MyEscape(ts);
      postData += "&submit=" + MyEscape("Submit changes");

      HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(Config.main.site + "ticket/" + id);
      req.PreAuthenticate = true;
      req.Credentials = new NetworkCredential(Config.main.user, Config.main.pass);
      byte[] send = System.Text.Encoding.ASCII.GetBytes(postData);
      req.Method = "POST";
      req.ContentType = "application/x-www-form-urlencoded";
      req.ContentLength = send.Length;
      req.Headers.Add("Cookie", cookie.Substring(0, cookie.IndexOf(';')));
      req.Referer = Config.main.site + "ticket/" + id;
      req.Headers.Add("Authorization", auth);

      using (Stream sout = req.GetRequestStream())
        sout.Write(send, 0, send.Length);

      string respPage = string.Empty;
      try
      {
        HttpWebResponse res = (HttpWebResponse)req.GetResponse();
        using (StreamReader r = new StreamReader(res.GetResponseStream()))
          respPage = r.ReadToEnd();
      }
      catch
      {
      }
    }

    bool LoadTicketsFromFile(string file)
    {
      List<Ticket> ticketList = null;
      if (!File.Exists(file))
        return false;
      try
      {
        XmlSerializer serializer = new XmlSerializer(typeof(List<Ticket>));
        using (StreamReader reader = new StreamReader(file))
          ticketList = serializer.Deserialize(reader) as List<Ticket>;
        if (ticketList == null)
          return false;
        foreach(Ticket ticket in ticketList)
          tickets.Add(ticket.GetValueRaw("ticket"), ticket);
        return true;
      }
      catch
      {
        return false;
      }
    }

    void SaveCustomTitlesToFile(string file)
    {
      List<string> list = new List<string>();
      foreach (KeyValuePair<string, string> it in customTitles)
        list.Add(string.Format("{0}: {1}", it.Key, it.Value));
      File.WriteAllLines(file, list.ToArray());
    }

    bool LoadCustomTitlesFromFile(string file)
    {
      if (!File.Exists(file))
        return false;
      string[] list = File.ReadAllLines(file);
      foreach (string customTitle in list)
      {
        string id = customTitle.Substring(0, customTitle.IndexOf(' ') - 1);
        string title = customTitle.Substring(customTitle.IndexOf(' ') + 1);
        customTitles.Add(id, title);
      }
      return true;
    }

    private void OnConfigLoaded()
    {
      hideClosedToolStripMenuItem.Checked = Config.main.hideTicketsWithClosedStatus;
      hideFixedToolStripMenuItem.Checked = Config.main.hideTicketsWithFixedStatus;
    }

    private void Form1_Load(object sender, EventArgs e)
    {
      Config.main = Config.Load("TracClient.cfg");
      OnConfigLoaded();
      LoadCustomTitlesFromFile("customtitles.txt");
      if (!LoadTicketsFromFile("tickets.xml"))
      {
        List<string> list = LoadTicketListString();
        for (int i = 1; i < list.Count; ++i)
        {
          Ticket ticket = new Ticket(list[0], list[i]);
          tickets.Add(ticket.GetValue("ticket"), ticket);
        }
      }
      Text = "Trac - " + Config.main.site;
      RefreshView();
      dataGridView1.ClearSelection();
      page = LoadPage(Config.main.site + "ticket/" + dataGridView1["ticket", 0].Value.ToString());
      bool enableSearchBox = true;
      foreach (KeyValuePair<string, Ticket> ticket in tickets)
        if (ticket.Value.Details.Length == 0)
        {
          enableSearchBox = false;
          break;
        }
      if (enableSearchBox)
        searchBox.Text = "";
      searchBox.Enabled = enableSearchBox;
    }

    void RefreshView()
    {
      FillGrid();
      status.Text = string.Format("{0} tickets in list", dataGridView1.RowCount);
    }

    private void webBrowser1_Navigating(object sender, WebBrowserNavigatingEventArgs e)
    {
      string url = e.Url.ToString();
      if (url == "about:blank")
        return;
      e.Cancel = true;
      if (url.Contains("/ticket/") && !url.Contains("attachment/"))
      {
        string id = url.Substring(url.LastIndexOf('/') + 1);
        GotoTicket(id);
        return;
      }
      WebRequest req = WebRequest.Create(url);
      req.Credentials = new NetworkCredential(Config.main.user, Config.main.pass);
      WebResponse resp = req.GetResponse();
      byte[] buffer = null;
      using (BinaryReader r = new BinaryReader(resp.GetResponseStream()))
        buffer = r.ReadBytes((int)resp.ContentLength);
      string file = url.Substring(url.LastIndexOf('/') + 1);
      file = Path.Combine(Path.GetTempPath(), file);
      File.WriteAllBytes(file, buffer);
      System.Diagnostics.Process.Start(file);
    }

    private void changeOwner_Click(object sender, EventArgs e)
    {
      ShowEdit(true, false);
    }

    private void addComment_Click(object sender, EventArgs e)
    {
      ShowEdit(false, false);
    }

    private void changeStatus_Click(object sender, EventArgs e)
    {
      ShowEdit(false, true);
    }

    void ShowEdit(bool changeOwner, bool chageStatus)
    {
      changeOwnerCheckbox.Checked = changeOwner;
      changeStatusCheckbox.Checked = chageStatus;
      topView.Panel2Collapsed = false;
      ownerCombo.Items.Clear();
      FillCombo(ref ownerCombo, "action_reassign_reassign_owner", Config.main.user);
      string id = dataGridView1["ticket", dataGridView1.SelectedRows[0].Cells[0].RowIndex].Value.ToString();
      FillCombo(ref statusCombo, "field_custom_stage", tickets[id].GetValueRaw("stage"));
      comment.Focus();
      comment.Clear();
    }

    string page = string.Empty;
    void FillCombo(ref ComboBox combo, string prefix, string default_)
    {
      combo.Items.Clear();
      string id = dataGridView1["ticket", dataGridView1.SelectedRows[0].Cells[0].RowIndex].Value.ToString();
      string s = page;
      if (s.Length == 0)
        s = LoadPage(Config.main.site + "ticket/" + id);
      int pos = s.IndexOf(prefix);
      if (pos > 0 && s[s.IndexOf('>', pos) - 1] != '/')
      {
        int pos2 = s.IndexOf("</select>", pos);
        s = s.Substring(pos, pos2 - pos);
        s = s.Replace(" selected=\"selected\"", "");
        for (pos = s.IndexOf("<option>"); pos != -1; pos = s.IndexOf("<option>", pos))
        {
          pos += 8;
          combo.Items.Add(s.Substring(pos, s.IndexOf("</option>", pos) - pos));
        }
      }
      else
        combo.Items.Add(default_);
    }

    private void dataGridView1_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.Control && e.KeyCode == Keys.G)
      {
        gotoBox.SelectAll();
        gotoBox.Focus();
        return;
      }
      if (e.KeyCode == Keys.F2 && dataGridView1.SelectedRows.Count == 1)
        SetTitle();
    }

    private void gotoBox_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Enter)
        GotoTicket(gotoBox.Text);
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
      List<string> list = LoadTicketListString();
      for (int i = 1; i < list.Count; ++i)
      {
        Ticket ticket = new Ticket(list[0], list[i]);
        string id = ticket.GetValueRaw("ticket");
        bool modified = false;
        if (tickets.ContainsKey(id))
        {
          modified = (ticket.GetValueRaw("_changetime") != tickets[id].GetValueRaw("_changetime"));
          if (modified)
            tickets[id] = ticket;
        }
        else
        {
          tickets.Add(id, ticket);
          modified = true;
          int row = dataGridView1.Rows.Add();
          dataGridView1["ticket", row].Value = id;
        }
        if (modified)
          SetTicketChanged(id, modified);
      }

      foreach (KeyValuePair<string, Ticket> ticket in tickets)
        if (ticket.Value.Details.Length == 0)
        {
          LoadTicket(ticket.Value.GetValueRaw("ticket"));
          return;
        }
      if (!searchBox.Enabled)
      {
        searchBox.Text = "";
        searchBox.Enabled = true;
      }
    }

    void SetTicketChanged(string id, bool changed)
    {
      int i = GetRow(id);
      if (i == -1)
        return;
      tickets[id].Unread = changed;
      int sumChanged = 0;
      foreach (KeyValuePair<string, Ticket> ticket in tickets)
        if (ticket.Value.Unread)
          ++sumChanged;
      if (sumChanged == 0)
        Text = "Trac - " + Config.main.site;
      else
        Text = "Trac - (" + sumChanged.ToString() + ") - " + Config.main.site;

      UpdateRowFromTicket(id, i);
    }

    private void cancelBtn_Click(object sender, EventArgs e)
    {
      topView.Panel2Collapsed = true;
    }

    private void changeOwnerCheckbox_CheckedChanged(object sender, EventArgs e)
    {
      ownerCombo.Enabled = changeOwnerCheckbox.Checked;
    }

    private void changeStatusCheckbox_CheckedChanged(object sender, EventArgs e)
    {
      statusCombo.Enabled = changeStatusCheckbox.Checked;
    }

    private void saveBtn_Click(object sender, EventArgs e)
    {
      topView.Panel2Collapsed = true;
      string status_ = statusCombo.Text;
      string owner_ = ownerCombo.Text;
      string comment_ = comment.Text;
      Dictionary<string, int> changedIds = new Dictionary<string, int>();
      foreach (DataGridViewRow row in dataGridView1.SelectedRows)
      {
        string id = dataGridView1["ticket", row.Cells[0].RowIndex].Value.ToString();
        changedIds.Add(id, row.Cells[0].RowIndex);
        ChangeTicket(id, comment_, status_, owner_);
      }

      List<string> list = LoadTicketListString();
      Dictionary<string, int> listIndex = new Dictionary<string, int>();
      for (int i = 1; i < list.Count; ++i)
      {
        int pos = list[i].IndexOf(",Owned,") + 7;
        string id = list[i].Substring(pos, list[i].IndexOf(',', pos) - pos);
        listIndex.Add(id, i);
      }

      List<int> removedTickets = new List<int>();
      foreach(KeyValuePair<string, int> changedId in changedIds)
      {
        string id = changedId.Key;
        if (!listIndex.ContainsKey(id))
        {
          // forwarded
          tickets.Remove(id);
          removedTickets.Add(changedId.Value);
        }
        else
        {
          // changed
          Ticket newTicket = new Ticket(list[0], list[listIndex[id]]);
          bool unread = tickets[id].Unread;
          tickets[id] = newTicket;
          tickets[id].Unread = unread;
          UpdateRowFromTicket(id, changedId.Value);
          LoadTicket(id);
        }
      }
      removedTickets.Sort();
      for (int row = removedTickets.Count - 1; row >= 0; --row)
        dataGridView1.Rows.RemoveAt(removedTickets[row]);
      SaveTickets();
    }

    private void dataGridView1_MouseDoubleClick(object sender, MouseEventArgs e)
    {
      string id = dataGridView1["ticket", dataGridView1.SelectedRows[0].Cells[0].RowIndex].Value.ToString();
      if (tickets[id].Unread)
        MarkAsRead();
      else
        ShowEdit(false, true);
    }

    private void SetTitle()
    {
      int row = dataGridView1.SelectedRows[0].Cells[0].RowIndex;
      string id = dataGridView1["ticket", row].Value.ToString();
      string title = (CustomTitle(id).Length > 0 ? CustomTitle(id) : tickets[id].GetValue("summary"));
      if (InputBox.Show("Custom Title", "Please enter custom title (will not modify ticket):", ref title) == DialogResult.OK)
      {
        if (tickets[id].GetValue("summary") == title)
          title = string.Empty;
        if (customTitles.ContainsKey(id))
          customTitles[id] = title;
        else
          customTitles.Add(id, title);
        UpdateRowFromTicket(id, row);
        SaveCustomTitlesToFile("customtitles.txt");
      }
    }

    private void setTitleBtn_Click(object sender, EventArgs e)
    {
      SetTitle();
    }

    private void MarkAsRead()
    {
      string id = dataGridView1["ticket", dataGridView1.SelectedRows[0].Cells[0].RowIndex].Value.ToString();
      SetTicketChanged(id, false);
      SaveTickets();
    }

    private void markAsReadBtn_Click(object sender, EventArgs e)
    {
      MarkAsRead();
    }

    private void searchBox_TextChanged(object sender, EventArgs e)
    {
      if (searchBox.Enabled)
        searchBoxTimer.Start();
    }

    private void searchBoxTimer_Tick(object sender, EventArgs e)
    {
      searchBoxTimer.Stop();
      filter = searchBox.Text;
      RefreshView();
    }

    private void hideClosedToolStripMenuItem_Click(object sender, EventArgs e)
    {
      Config.main.hideTicketsWithClosedStatus = hideClosedToolStripMenuItem.Checked;
      Config.main.Save();
      RefreshView();
      RefreshTicket();
    }

    private void hideFixedToolStripMenuItem_Click(object sender, EventArgs e)
    {
      Config.main.hideTicketsWithFixedStatus = hideFixedToolStripMenuItem.Checked;
      Config.main.Save();
      RefreshView();
      RefreshTicket();
    }
  }
}